﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Xml;
using MLog.Helpers;
using UnityEngine;
using UnityEngine.Events;

namespace MLog {
    public struct Attr {
        public string Name { set; get; }
        public string Value { set; get; }

        public Attr(string name, string value) : this() {
            if (name == null) throw new ArgumentNullException("name");
            Name  = name;
            Value = value;
        }

        public override string ToString() {
            return string.Format("Name: {0}, Value: {1}", Name, Value);
        }
    }

    public struct Node {
        public string Name { set; get; }
        public string Value { set; get; }
        public List<Node> Child { set; get; }
        public List<Attr> Attrs { set; get; }

        public string GetAttr(string name, string defaultValue = null) {
            var attrs = Attrs;
            if (attrs == null) return defaultValue;
            for (var i = 0; i < attrs.Count; i++) {
                var attr = attrs[i];
                if (name.Equals(attr.Name)) return attr.Value;
            }

            return defaultValue;
        }

        public void SetAttr(string name, string value) {
            if (Attrs == null)
                Attrs = new List<Attr>();
            else
                for (var i = 0; i < Attrs.Count; i++)
                    if (Attrs[i].Name.Equals(name)) {
                        Attrs.RemoveAt(i);
                        break;
                    }

            Attrs.Add(new Attr(name, value));
        }


        public Node Copy() {
            var node = new Node();

            node.Name  = Name;
            node.Value = Value;
            if (Attrs != null) node.Attrs = new List<Attr>(Attrs);

            if (Child != null) {
                node.Child = new List<Node>();
                for (var i = 0; i < Child.Count; i++) node.Child.Add(Child[i].Copy());
            }

            return node;
        }

        public override string ToString() {
            return string.Format("Name: {0}, Value: {1}, Child: {2}, Attrs: {3}", Name, Value,
                FormatterUtils.FormatArray(Child == null ? null : Child.ToArray()),
                FormatterUtils.FormatArray(Attrs == null ? null : Attrs.ToArray()));
        }
    }

    public class LoggerConfiguration {
        private static readonly List<Node> LoggerConfigs = new List<Node>();

        private static readonly Dictionary<string, Node> Loggers = new Dictionary<string, Node>();

        public static void Initialize(UnityAction completeCallback) {
            ParseConfiguration(GetDefaultConfiguration(), LoggerConfigs, Loggers);
            var customConfigurations = GetCustomConfigurations();
            if (customConfigurations != null) {
                foreach (var configuration in customConfigurations) {
                    var config      = new List<Node>();
                    var loggerNodes = new Dictionary<string, Node>();
                    ParseConfiguration(configuration, config, loggerNodes);
                    MergeConfiguration(LoggerConfigs, config);
                    MergeConfiguration(Loggers, loggerNodes);
                }
            }

            if (completeCallback != null) completeCallback();
        }

        private static IEnumerable<string> GetCustomConfigurations() {
            var textAssets = Resources.LoadAll<TextAsset>("MLog/mlog");
            if (textAssets == null) {
                textAssets = Resources.LoadAll<TextAsset>("MLog/mlog.xml");
            }

            if (textAssets != null) {
                var datas = new string[textAssets.Length];
                for (var i = 0; i < textAssets.Length; i++) {
                    datas[i] = textAssets[i].text;
                }

                return datas;
            }

            return null;
        }

        private static string GetDefaultConfiguration() {
            var mlog = Resources.Load<TextAsset>("mlog_defs");
            if (mlog == null) {
                mlog = Resources.Load<TextAsset>("mlog_defs.xml");
            }

            if (mlog != null) {
                return mlog.text;
            }

            throw new MLogException("MLog: mlog_defs.xml not found!");
        }

        private static void MergeConfiguration(List<Node> target, List<Node> nodes) {
            foreach (var node in nodes) {
                var tag            = node.Name;
                var name           = node.GetAttr("name");
                var namespaceValue = node.GetAttr("namespace");
                for (var i = 0; i < target.Count; i++) {
                    var flag = false;
                    var n2   = target[i];
                    if (Equals(n2.Name, tag)) {
                        if (Constants.LoggerName.Equals(tag)) {
                            if (Equals(n2.GetAttr("namespace"), namespaceValue)) flag = true;
                        }
                        else if (Constants.AppenderName.Equals(tag)) {
                            if (Equals(n2.GetAttr("name"), name)) flag = true;
                        }
                        else if (Constants.RootName.Equals(tag)) {
                            flag = true;
                        }
                    }

                    if (flag) {
                        target.RemoveAt(i);
                        break;
                    }
                }

                target.Add(node);
            }
        }

        private static void MergeConfiguration(Dictionary<string, Node> target, Dictionary<string, Node> nodes) {
            foreach (var pair in nodes) {
                target[pair.Key] = pair.Value;
            }
        }

        private static void ParseConfiguration(string data, List<Node> config, Dictionary<string, Node> loggers) {
            var document = new XmlDocument();
            document.LoadXml(data);
            var root = document.DocumentElement;

#if UNITY_5_3_OR_NEWER
            UnityEngine.Assertions.Assert.IsNotNull(root, "mlog xml error!");
#endif
            var childNodes = root.ChildNodes;
            for (var i = 0; i < childNodes.Count; i++) {
                var node = childNodes[i];
                if ("#comment".Equals(node.Name) || "#text".Equals(node.Name)) continue;
                var n = ParseNode(node);
                config.Add(n);
                if (Constants.LoggerName.Equals(n.Name)) {
#if UNITY_5_3_OR_NEWER
                    UnityEngine.Assertions.Assert.IsNotNull(n.GetAttr("namespace"), "mlog xml error!");
#endif
                    loggers[n.GetAttr("namespace")] = n;
                }
                else if (Constants.RootName.Equals(n.Name)) {
                    loggers[Constants.RootName] = n;
                }
            }
        }


        private static Node ParseNode(XmlNode node) {
            var n = new Node();
            n.Name  = node.Name;
            n.Value = node.InnerText;
            var attrs = node.Attributes;
            if (attrs != null) {
                n.Attrs = new List<Attr>();
                for (var i = 0; i < attrs.Count; i++) {
                    var attr = attrs[i];
                    n.Attrs.Add(new Attr(attr.Name, attr.Value));
                }
            }

            var childNodes = node.ChildNodes;
            if (childNodes.Count > 0) {
                n.Child = new List<Node>();
                for (var i = 0; i < childNodes.Count; i++) n.Child.Add(ParseNode(childNodes[i]));
            }

            return n;
        }

        private static void removeChild(Node n, string tag) {
            var child = n.Child;
            if (child == null) return;
            for (var i = 0; i < child.Count; i++)
                if (tag.Equals(child[i].Name)) {
                    n.Child.RemoveAt(i);
                    return;
                }
        }

        private static Node FindNode(string tag, string nameAttr, string nameValue) {
            var nodes = LoggerConfigs;
            for (var i = 0; i < nodes.Count; i++) {
                var node = nodes[i];
                if (Equals(tag, node.Name) && Equals(node.GetAttr(nameAttr), nameValue)) return node;
            }

            throw new MLogException(string.Format("config {0}-{1}:{2} not found", tag, nameAttr, nameValue));
        }

        public static Node GetLogger(string name) {
            return FindNode(Constants.LoggerName, "namespace", name);
        }

        public static Node GetLoggerNearest(string name) {
            if (Loggers.ContainsKey(name)) return Loggers[name];

            var    best    = -1;
            string bestKey = null;
            var    keys    = Loggers.Keys;
            foreach (var key in keys)
                if (name.StartsWith(key)) {
                    var sub = name.Length - key.Length;
                    if (sub < best || best == -1) {
                        best    = sub;
                        bestKey = key;
                    }
                }

            if (bestKey != null) return Loggers[bestKey];

            if (Loggers.ContainsKey(Constants.RootName)) return Loggers[Constants.RootName];

            throw new MLogException(string.Format("MLog: xml error! {0} not found", name));
        }

        public static Node GetAppender(string name) {
            return FindNode(Constants.AppenderName, "name", name);
        }

        public static PropertyInfo GetPropertyName(Type target, Node property) {
            var origin = property.Name;
            var sp     = origin.Split('-');
            var name   = new StringBuilder();

            for (var i = 0; i < sp.Length; i++) {
                var s = sp[i];
                name.Append(char.ToUpper(s[0]));
                if (s.Length > 1) name.Append(s.Substring(1));
            }

            var originType = target;
            while (target != null) {
                var info = target.GetProperty(name.ToString(), BindingFlags.Public | BindingFlags.Instance);
                if (info != null) return info;
                target = target.BaseType;
            }

            throw new MLogException(string.Format("MLog: {0} not found {1}==> {2}", originType.FullName, origin, name));
        }
    }
}